/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.monitor;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.impl.wsdl.mock.DispatchException;
import com.eviware.soapui.impl.wsdl.support.soap.SoapMessageBuilder;
import com.eviware.soapui.impl.wsdl.support.soap.SoapVersion;
import com.eviware.soapui.model.mock.MockResult;
import com.eviware.soapui.model.mock.MockRunner;
import com.eviware.soapui.model.mock.MockService;
import com.eviware.soapui.model.propertyexpansion.PropertyExpander;
import com.eviware.soapui.settings.HttpSettings;
import com.eviware.soapui.settings.SSLSettings;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.Tools;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.log.JettyLogger;
import org.apache.log4j.Logger;
import org.mortbay.component.AbstractLifeCycle;
import org.mortbay.io.Connection;
import org.mortbay.io.EndPoint;
import org.mortbay.io.nio.SelectChannelEndPoint;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.HttpConnection;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.RequestLog;
import org.mortbay.jetty.Response;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.AbstractHandler;
import org.mortbay.jetty.handler.RequestLogHandler;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.security.SslSocketConnector;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Core Mock-Engine hosting a Jetty web server
 *
 * @author ole.matzura
 */

public class JettyMockEngine implements MockEngine {
    public final static Logger log = Logger.getLogger(JettyMockEngine.class);

    private Server server;
    private Map<Integer, Map<String, List<MockRunner>>> runners = new HashMap<Integer, Map<String, List<MockRunner>>>();
    private Map<Integer, SoapUIConnector> connectors = new HashMap<Integer, SoapUIConnector>();
    private List<MockRunner> mockRunners = new CopyOnWriteArrayList<MockRunner>();

    private SslSocketConnector sslConnector;

    private boolean addedSslConnector;

    public JettyMockEngine() {
        System.setProperty("org.mortbay.log.class", JettyLogger.class.getName());
    }

    public boolean hasRunningMock(MockService mockService) {
        for (MockRunner runner : mockRunners) {
            if (runner.getMockContext().getMockService() == mockService) {
                return true;
            }
        }

        return false;
    }

    public synchronized void startMockService(MockRunner runner) throws Exception {
        if (server == null) {
            initServer();
        }

        synchronized (server) {
            MockService mockService = runner.getMockContext().getMockService();
            int port = mockService.getPort();

            if (SoapUI.getSettings().getBoolean(SSLSettings.ENABLE_MOCK_SSL) && !addedSslConnector) {
                updateSslConnectorSettings();
                server.addConnector(sslConnector);
                addedSslConnector = true;
            } else {
                if (addedSslConnector) {
                    server.removeConnector(sslConnector);
                }

                addedSslConnector = false;
            }

            if (!runners.containsKey(port)) {
                SoapUIConnector connector = new SoapUIConnector();
                PropertySupport.applySystemProperties(connector, "soapui.mock.connector", runner.getMockContext().getMockService());

                connector.setPort(port);
                if (sslConnector != null) {
                    connector.setConfidentialPort(sslConnector.getPort());
                }

                if (mockService.getBindToHostOnly()) {
                    String host = PropertyExpander.expandProperties(mockService, mockService.getHost());
                    if (StringUtils.hasContent(host)) {
                        connector.setHost(host);
                    }
                }

                boolean wasRunning = server.isRunning();

                if (wasRunning) {
                    server.stop();
                }

                server.addConnector(connector);
                try {
                    server.start();
                } catch (RuntimeException e) {
                    UISupport.showErrorMessage(e);

                    server.removeConnector(connector);
                    if (wasRunning) {
                        server.start();
                        return;
                    }
                }

                connectors.put(port, connector);
                runners.put(port, new HashMap<String, List<MockRunner>>());
            }

            Map<String, List<MockRunner>> map = runners.get(port);
            String path = mockService.getPath();
            if (!map.containsKey(path)) {
                map.put(path, new ArrayList<MockRunner>());
            }
            map.get(path).add(runner);
            mockRunners.add(runner);

            log.info("Started mockService [" + mockService.getName() + "] on port [" + port + "] at path [" + path + "]");
        }
    }

    private void initServer() throws Exception {
        server = new Server();
        server.setThreadPool(new SoapUIJettyThreadPool());
        server.setHandler(new ServerHandler());

        RequestLogHandler logHandler = new RequestLogHandler();
        logHandler.setRequestLog(new MockRequestLog());
        server.addHandler(logHandler);

        sslConnector = new SslSocketConnector();
        sslConnector.setMaxIdleTime(30000);
    }

    private void updateSslConnectorSettings() {
        sslConnector.setKeystore(SoapUI.getSettings().getString(SSLSettings.MOCK_KEYSTORE, null));
        sslConnector.setPassword(SoapUI.getSettings().getString(SSLSettings.MOCK_PASSWORD, null));
        sslConnector.setKeyPassword(SoapUI.getSettings().getString(SSLSettings.MOCK_KEYSTORE_PASSWORD, null));
        String trustStore = SoapUI.getSettings().getString(SSLSettings.MOCK_TRUSTSTORE, null);
        if (StringUtils.hasContent(trustStore)) {
            sslConnector.setTruststore(trustStore);
            sslConnector.setTrustPassword(SoapUI.getSettings().getString(SSLSettings.MOCK_TRUSTSTORE_PASSWORD, null));
        }

        sslConnector.setPort((int) SoapUI.getSettings().getLong(SSLSettings.MOCK_PORT, 443));
        sslConnector.setNeedClientAuth(SoapUI.getSettings().getBoolean(SSLSettings.CLIENT_AUTHENTICATION));
    }

    public void stopMockService(MockRunner runner) {
        synchronized (server) {
            MockService mockService = runner.getMockContext().getMockService();
            final Integer port = mockService.getPort();
            Map<String, List<MockRunner>> map = runners.get(port);

            if (map == null || !map.containsKey(mockService.getPath())) {
                return;
            }

            map.get(mockService.getPath()).remove(runner);
            if (map.get(mockService.getPath()).isEmpty()) {
                map.remove(mockService.getPath());
            }

            mockRunners.remove(runner);

            log.info("Stopped MockService [" + mockService.getName() + "] on port [" + port + "]");

            if (map.isEmpty() && !SoapUI.getSettings().getBoolean(HttpSettings.LEAVE_MOCKENGINE)) {
                SoapUIConnector connector = connectors.get(port);
                if (connector == null) {
                    log.warn("Missing connectors on port [" + port + "]");
                    return;
                }

                try {
                    log.info("Stopping connector on port " + port);
                    if (!connector.waitUntilIdle(5000)) {
                        log.warn("Failed to wait for idle.. stopping connector anyway..");
                    }
                    connector.stop();
                } catch (Exception e) {
                    SoapUI.logError(e);
                }
                server.removeConnector(connector);
                runners.remove(port);
                if (runners.isEmpty()) {
                    try {
                        log.info("No more connectors.. stopping server");
                        server.stop();
                        if (sslConnector != null) {
                            // server.removeConnector( sslConnector );
                            // sslConnector.stop();
                            // sslConnector = null;
                        }
                    } catch (Exception e) {
                        SoapUI.logError(e);
                    }
                }
            }
        }
    }

    private class SoapUIConnector extends SelectChannelConnector {
        private Set<HttpConnection> connections = new HashSet<HttpConnection>();

        @Override
        protected void connectionClosed(HttpConnection arg0) {
            super.connectionClosed(arg0);
            connections.remove(arg0);
        }

        @Override
        protected void connectionOpened(HttpConnection arg0) {
            super.connectionOpened(arg0);
            connections.add(arg0);
        }

        @Override
        protected Connection newConnection(SocketChannel socketChannel, SelectChannelEndPoint selectChannelEndPoint) {
            return new SoapUIHttpConnection(SoapUIConnector.this, selectChannelEndPoint, getServer());
        }

        public boolean waitUntilIdle(long maxWait) throws Exception {
            while (maxWait > 0 && hasActiveConnections()) {
                System.out.println("Waiting for active connections to finish..");
                Thread.sleep(500);
                maxWait -= 500;
            }

            return !hasActiveConnections();
        }

        private boolean hasActiveConnections() {
            for (HttpConnection connection : connections) {
                if (!connection.isIdle()) {
                    return true;
                }
            }

            return false;
        }
    }

    private class SoapUIHttpConnection extends HttpConnection {
        private CapturingServletInputStream capturingServletInputStream;
        private BufferedServletInputStream bufferedServletInputStream;
        private CapturingServletOutputStream capturingServletOutputStream;

        public SoapUIHttpConnection(Connector connector, EndPoint endPoint, Server server) {
            super(connector, endPoint, server);
        }

        @Override
        public ServletInputStream getInputStream() {
            if (SoapUI.getSettings().getBoolean(HttpSettings.ENABLE_MOCK_WIRE_LOG)) {
                if (capturingServletInputStream == null) {
                    capturingServletInputStream = new CapturingServletInputStream(super.getInputStream());
                    bufferedServletInputStream = new BufferedServletInputStream(capturingServletInputStream);
                }
            } else {
                bufferedServletInputStream = new BufferedServletInputStream(super.getInputStream());
            }

            return bufferedServletInputStream;
        }

        @Override
        public ServletOutputStream getOutputStream() {
            if (SoapUI.getSettings().getBoolean(HttpSettings.ENABLE_MOCK_WIRE_LOG)) {
                if (capturingServletOutputStream == null) {
                    capturingServletOutputStream = new CapturingServletOutputStream(super.getOutputStream());
                }
                return capturingServletOutputStream;
            } else {
                return super.getOutputStream();
            }
        }
    }

    private class BufferedServletInputStream extends ServletInputStream {
        private InputStream source = null;
        private byte[] data = null;
        private InputStream buffer1 = null;

        public BufferedServletInputStream(InputStream is) {
            super();
            source = is;
        }

        public InputStream getBuffer() throws IOException {
            if (source.available() > 0) {
                // New request content available
                data = null;
            }
            if (data == null) {
                ByteArrayOutputStream out = Tools.readAll(source, Tools.READ_ALL);
                data = out.toByteArray();
            }
            if (buffer1 == null) {
                buffer1 = new ByteArrayInputStream(data);
            }
            return buffer1;
        }

        public int read() throws IOException {
            return getBuffer().read();
        }

        public int readLine(byte[] b, int off, int len) throws IOException {

            if (len <= 0) {
                return 0;
            }
            int count = 0, c;

            while ((c = read()) != -1) {
                b[off++] = (byte) c;
                count++;
                if (c == '\n' || count == len) {
                    break;
                }
            }
            return count > 0 ? count : -1;
        }

        public int read(byte[] b) throws IOException {
            return getBuffer().read(b);
        }

        public int read(byte[] b, int off, int len) throws IOException {
            return getBuffer().read(b, off, len);
        }

        public long skip(long n) throws IOException {
            return getBuffer().skip(n);
        }

        public int available() throws IOException {
            return getBuffer().available();
        }

        public void close() throws IOException {
            getBuffer().close();
        }

        public void mark(int readlimit) {
            // buffer.mark( readlimit );
        }

        public boolean markSupported() {
            return false;
        }

        public void reset() throws IOException {
            buffer1 = null;
        }
    }

    private class CapturingServletOutputStream extends ServletOutputStream {
        private ServletOutputStream outputStream;
        private ByteArrayOutputStream captureOutputStream = new ByteArrayOutputStream();

        public CapturingServletOutputStream(ServletOutputStream outputStream) {
            this.outputStream = outputStream;
        }

        public void print(String s) throws IOException {
            outputStream.print(s);
        }

        public void print(boolean b) throws IOException {
            outputStream.print(b);
        }

        public void print(char c) throws IOException {
            outputStream.print(c);
        }

        public void print(int i) throws IOException {
            outputStream.print(i);
        }

        public void print(long l) throws IOException {
            outputStream.print(l);
        }

        public void print(float v) throws IOException {
            outputStream.print(v);
        }

        public void print(double v) throws IOException {
            outputStream.print(v);
        }

        public void println() throws IOException {
            outputStream.println();
        }

        public void println(String s) throws IOException {
            outputStream.println(s);
        }

        public void println(boolean b) throws IOException {
            outputStream.println(b);
        }

        public void println(char c) throws IOException {
            outputStream.println(c);
        }

        public void println(int i) throws IOException {
            outputStream.println(i);
        }

        public void println(long l) throws IOException {
            outputStream.println(l);
        }

        public void println(float v) throws IOException {
            outputStream.println(v);
        }

        public void println(double v) throws IOException {
            outputStream.println(v);
        }

        public void write(int b) throws IOException {
            captureOutputStream.write(b);
            outputStream.write(b);
        }

        public void write(byte[] b) throws IOException {
            captureOutputStream.write(b);
            outputStream.write(b);
        }

        public void write(byte[] b, int off, int len) throws IOException {
            captureOutputStream.write(b, off, len);
            outputStream.write(b, off, len);
        }

        public void flush() throws IOException {
            outputStream.flush();
        }

        public void close() throws IOException {
            outputStream.close();
            // log.info( "Closing output stream, captured: " +
            // captureOutputStream.toString() );
        }
    }

    private class CapturingServletInputStream extends ServletInputStream {
        private ServletInputStream inputStream;
        private ByteArrayOutputStream captureOutputStream = new ByteArrayOutputStream();

        public CapturingServletInputStream(ServletInputStream inputStream) {
            this.inputStream = inputStream;
        }

        public int read() throws IOException {
            int i = inputStream.read();
            captureOutputStream.write(i);
            return i;
        }

        public int readLine(byte[] bytes, int i, int i1) throws IOException {
            int result = inputStream.readLine(bytes, i, i1);
            captureOutputStream.write(bytes, i, i1);
            return result;
        }

        public int read(byte[] b) throws IOException {
            int i = inputStream.read(b);
            captureOutputStream.write(b);
            return i;
        }

        public int read(byte[] b, int off, int len) throws IOException {
            int result = inputStream.read(b, off, len);
            if (result != -1) {
                captureOutputStream.write(b, off, result);
            }
            return result;
        }

        public long skip(long n) throws IOException {
            return inputStream.skip(n);
        }

        public int available() throws IOException {
            return inputStream.available();
        }

        public void close() throws IOException {
            inputStream.close();
        }

        public void mark(int readLimit) {
            inputStream.mark(readLimit);
        }

        public boolean markSupported() {
            return inputStream.markSupported();
        }

        public void reset() throws IOException {
            inputStream.reset();
        }
    }

    private class ServerHandler extends AbstractHandler {
        public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch)
                throws IOException, ServletException {
            // find mockService
            Map<String, List<MockRunner>> map = runners.get(request.getLocalPort());

            // ssl?
            if (map == null && sslConnector != null && request.getLocalPort() == sslConnector.getPort()) {
                for (Map<String, List<MockRunner>> runnerMap : runners.values()) {
                    if (runnerMap.containsKey(request.getPathInfo())) {
                        map = runnerMap;
                        break;
                    }
                }
            }

            if (map != null) {
                List<MockRunner> wsdlMockRunners = map.get(request.getPathInfo());
                if (wsdlMockRunners == null) {
                    String bestMatchedRootPath = "";

                    for (String root : map.keySet()) {
                        if (request.getPathInfo().startsWith(root) && root.length() > bestMatchedRootPath.length()) {
                            bestMatchedRootPath = root;
                            wsdlMockRunners = map.get(root);
                        }
                    }
                }

                if (wsdlMockRunners != null) {
                    try {
                        DispatchException ex = null;
                        MockResult result = null;

                        for (MockRunner wsdlMockRunner : wsdlMockRunners) {
                            if (!wsdlMockRunner.isRunning()) {
                                continue;
                            }

                            try {
                                result = wsdlMockRunner.dispatchRequest(request, response);
                                if (result != null) {
                                    result.finish();
                                    break;
                                }
                            } catch (DispatchException e) {
                                ex = e;
                            }
                        }

                        if (ex != null && result == null) {
                            throw ex;
                        }
                    } catch (Exception e) {
                        SoapUI.logError(e);

                        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                        response.setContentType("text/html");
                        response.getWriter().print(
                                SoapMessageBuilder.buildFault("Server", e.getMessage(), SoapVersion.Utils
                                        .getSoapVersionForContentType(request.getContentType(), SoapVersion.Soap11)));
                        // throw new ServletException( e );
                    }
                } else {
                    printMockServiceList(response);
                }
            } else {
                printMockServiceList(response);
            }

            response.flushBuffer();
        }

        private void printMockServiceList(HttpServletResponse response) throws IOException {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("text/html");

            MockRunner[] mockRunners = getMockRunners();
            PrintWriter out = response.getWriter();
            out.print("<html><body><p>There are currently " + mockRunners.length + " running SoapUI MockServices</p><ul>");

            for (MockRunner mockRunner : mockRunners) {
                out.print("<li><a href=\"");
                out.print(mockRunner.getMockContext().getMockService().getPath() + "?WSDL");
                out.print("\">" + mockRunner.getMockContext().getMockService().getName() + "</a></li>");
            }

            out.print("</ul></p></body></html>");
        }
    }

    public MockRunner[] getMockRunners() {
        return mockRunners.toArray(new MockRunner[mockRunners.size()]);
    }

    private class MockRequestLog extends AbstractLifeCycle implements RequestLog {
        public void log(Request request, Response response) {
            if (!SoapUI.getSettings().getBoolean(HttpSettings.ENABLE_MOCK_WIRE_LOG)) {
                return;
            }

            if (SoapUI.getLogMonitor() == null || SoapUI.getLogMonitor().getLogArea("jetty log") == null
                    || SoapUI.getLogMonitor().getLogArea("jetty log").getLoggers() == null) {
                return;
            }

            Logger logger = SoapUI.getLogMonitor().getLogArea("jetty log").getLoggers()[0];

            try {
                ServletInputStream inputStream = request.getInputStream();
                if (inputStream instanceof CapturingServletInputStream) {
                    ByteArrayOutputStream byteArrayOutputStream = ((CapturingServletInputStream) inputStream).captureOutputStream;
                    String str = request.toString() + byteArrayOutputStream.toString();
                    BufferedReader reader = new BufferedReader(new StringReader(str));
                    ((CapturingServletInputStream) inputStream).captureOutputStream = new ByteArrayOutputStream();

                    String line = reader.readLine();
                    while (line != null) {
                        logger.info(">> \"" + line + "\"");
                        line = reader.readLine();
                    }
                }
            } catch (Exception e) {
                SoapUI.logError(e);
            }

            try {
                ServletOutputStream outputStream = response.getOutputStream();
                if (outputStream instanceof CapturingServletOutputStream) {
                    ByteArrayOutputStream byteArrayOutputStream = ((CapturingServletOutputStream) outputStream).captureOutputStream;
                    String str = request.toString() + byteArrayOutputStream.toString();
                    BufferedReader reader = new BufferedReader(new StringReader(str));
                    ((CapturingServletOutputStream) outputStream).captureOutputStream = new ByteArrayOutputStream();

                    String line = reader.readLine();
                    while (line != null) {
                        logger.info("<< \"" + line + "\"");
                        line = reader.readLine();
                    }
                }
            } catch (Exception e) {
                SoapUI.logError(e);
            }
        }
    }
}
